import { TSESTree, AST_TOKEN_TYPES } from '@typescript-eslint/types';
import * as fs from 'fs';
import * as path from 'path';
import { format, resolveConfig } from 'prettier';
import rimraf from 'rimraf';
import * as ts from 'typescript';
import { ScopeManager, Variable } from '../src';
import { parseAndAnalyze } from '../tests/util/parse';

const libMap = new Map(ts.libMap);
// add the "full" libs as well - these are used by the default config resolution system
for (const [lib] of ts.libMap) {
  if (
    (/^es2\d{3}$/.test(lib) || lib === 'esnext') &&
    // there's no "full" lib for es2015
    lib !== 'es2015'
  ) {
    libMap.set(`${lib}.full`, `lib.${lib}.full.d.ts`);
  }
}
// the base lib used when the target is unknown
libMap.set('lib', 'lib.d.ts');

function addAutoGeneratedComment(code: string[]): string {
  return [
    '// THIS CODE WAS AUTOMATICALLY GENERATED',
    '// DO NOT EDIT THIS CODE BY HAND',
    '// YOU CAN REGENERATE IT USING yarn generate:lib',
    '',
    ...code,
  ].join('\n');
}

const PRETTIER_CONFIG = resolveConfig.sync(__dirname);
const TS_LIB_FOLDER = path.resolve(
  __dirname,
  '..',
  '..',
  '..',
  'node_modules',
  'typescript',
  'lib',
);
const OUTPUT_FOLDER = path.resolve(__dirname, '..', 'src', 'lib');
const TYPES_FILE = path.resolve(
  __dirname,
  '..',
  '..',
  'types',
  'src',
  'lib.ts',
);

function formatCode(code: string[]): string {
  return format(addAutoGeneratedComment(code), {
    parser: 'typescript',
    ...PRETTIER_CONFIG,
  });
}

function sanitize(name: string): string {
  return name.replace(/\./g, '_');
}

function getVariablesFromScope(scopeManager: ScopeManager): Variable[] {
  const scope = scopeManager.globalScope!.childScopes[0];
  const variables: Variable[] = [];
  for (const variable of scope.variables) {
    if (variable.isTypeVariable) {
      variables.push(variable);
    }
  }

  return variables;
}

const REFERENCE_REGEX = /\/ <reference lib="(.+)" \/>/;
function getReferences(
  ast: TSESTree.Program & { comments?: TSESTree.Comment[] },
): Set<string> {
  const comments = ast.comments!.filter(
    c =>
      c.type === AST_TOKEN_TYPES.Line &&
      c.value.startsWith('/ <reference lib="'),
  );

  const references = new Set<string>();
  for (const comment of comments) {
    const match = REFERENCE_REGEX.exec(comment.value);
    if (!match) {
      continue;
    }

    references.add(match[1]);
  }
  return references;
}

function main(): void {
  try {
    rimraf.sync(OUTPUT_FOLDER);
  } catch {
    // ignored
  }
  try {
    fs.mkdirSync(OUTPUT_FOLDER);
  } catch {
    // ignored
  }

  for (const [libName, filename] of libMap) {
    const libPath = path.join(TS_LIB_FOLDER, filename);
    const { ast, scopeManager } = parseAndAnalyze(
      fs.readFileSync(libPath, 'utf8'),
      {
        // we don't want any libs
        lib: [],
        sourceType: 'module',
      },
      {
        comment: true,
        loc: true,
        range: true,
      },
    );

    const code = [`export const ${sanitize(libName)} = {`];

    const references = getReferences(ast);
    if (references.size > 0) {
      // add a newline before the export
      code.unshift('');
    }

    // import and spread all of the references
    const imports = [
      "import { ImplicitLibVariableOptions } from '../variable';",
    ];
    for (const reference of references) {
      const name = sanitize(reference);
      imports.push(`import { ${name} } from './${reference}'`);
      code.push(`...${name},`);
    }

    if (imports.length > 0) {
      code.unshift(...imports, '');
    }

    // mark all of the variables
    const variables = getVariablesFromScope(scopeManager);
    for (const variable of variables) {
      code.push(
        `'${variable.name}': ${JSON.stringify({
          eslintImplicitGlobalSetting: 'readonly',
          isTypeVariable: variable.isTypeVariable,
          isValueVariable: variable.isValueVariable,
          name: variable.name,
        })},`,
      );
    }
    code.push('} as Record<string, ImplicitLibVariableOptions>;');

    const formattedCode = formatCode(code);
    fs.writeFileSync(path.join(OUTPUT_FOLDER, `${libName}.ts`), formattedCode);

    console.log(
      'Wrote',
      variables.length,
      'variables, and',
      references.size,
      'references for',
      libName,
    );
  }

  // generate and write a barrel file
  const barrelImports = []; // use a separate way so everything is in the same order
  const barrelCode = ['', `const lib = {`];
  for (const lib of libMap.keys()) {
    const name = sanitize(lib);
    if (name === 'lib') {
      barrelImports.push(`import { lib as libBase } from './${lib}'`);
      barrelCode.push(`'${lib}': libBase,`);
    } else {
      barrelImports.push(`import { ${name} } from './${lib}'`);
      barrelCode.push(lib === name ? `${lib},` : `'${lib}': ${name},`);
    }
  }
  barrelCode.unshift(...barrelImports);
  barrelCode.push('} as const;');

  barrelCode.push('', 'export { lib };');

  const formattedBarrelCode = formatCode(barrelCode);

  fs.writeFileSync(path.join(OUTPUT_FOLDER, 'index.ts'), formattedBarrelCode);
  console.log('Wrote barrel file');

  // generate a string union type for the lib names

  const libUnionCode = [
    `type Lib = ${Array.from(libMap.keys())
      .map(k => `'${k}'`)
      .join(' | ')};`,
    '',
    'export { Lib };',
  ];
  const formattedLibUnionCode = formatCode(libUnionCode);

  fs.writeFileSync(TYPES_FILE, formattedLibUnionCode);
  console.log('Wrote Lib union type file');
}

main();
